summaryrefslogtreecommitdiff
path: root/ui/routes/(app)/c/[conversation]
diff options
context:
space:
mode:
Diffstat (limited to 'ui/routes/(app)/c/[conversation]')
-rw-r--r--ui/routes/(app)/c/[conversation]/+page.svelte121
1 files changed, 121 insertions, 0 deletions
diff --git a/ui/routes/(app)/c/[conversation]/+page.svelte b/ui/routes/(app)/c/[conversation]/+page.svelte
new file mode 100644
index 0000000..e6cd845
--- /dev/null
+++ b/ui/routes/(app)/c/[conversation]/+page.svelte
@@ -0,0 +1,121 @@
+<script>
+ import { DateTime } from 'luxon';
+ import { page } from '$app/state';
+ import MessageInput from '$lib/components/MessageInput.svelte';
+ import MessageRun from '$lib/components/MessageRun.svelte';
+ import Message from '$lib/components/Message.svelte';
+ import { runs } from '$lib/runs.js';
+
+ const { data } = $props();
+ const { session, outbox } = data;
+ let activeConversation;
+
+ const conversationId = $derived(page.params.conversation);
+ const conversation = $derived(
+ session.conversations.find((conversation) => conversation.id === conversationId),
+ );
+ const messages = $derived(
+ session.messages.filter((message) => message.conversation === conversationId),
+ );
+ const unsent = $derived(
+ outbox.messages.filter((message) => message.conversation === conversationId),
+ );
+ const deleted = $derived(outbox.deleted.map((message) => message.messageId));
+ const unsentSkeletons = $derived(
+ unsent.map((message) => message.toSkeleton($state.snapshot(session.currentUser))),
+ );
+ const messageRuns = $derived(runs(messages.concat(unsentSkeletons), session.currentUser));
+
+ function inView(parentElement, element) {
+ const parRect = parentElement.getBoundingClientRect();
+ const parentTop = parRect.top;
+ const parentBottom = parRect.bottom;
+
+ const elRect = element.getBoundingClientRect();
+ const elementTop = elRect.top;
+ const elementBottom = elRect.bottom;
+
+ return parentTop < elementTop && parentBottom > elementBottom;
+ }
+
+ function getLastVisibleMessage() {
+ if (activeConversation) {
+ const childElements = activeConversation.getElementsByClassName('message');
+ const lastInView = Array.from(childElements)
+ .reverse()
+ .find((el) => {
+ return inView(activeConversation, el);
+ });
+ return lastInView;
+ }
+ }
+
+ function setLastRead() {
+ const lastInView = getLastVisibleMessage();
+ const at = !!lastInView ? DateTime.fromISO(lastInView.dataset.at) : conversation?.at;
+ if (!!at) {
+ session.local.updateLastReadAt(conversationId, at);
+ }
+ }
+
+ $effect(() => {
+ const _ = session.messages;
+ setLastRead();
+ });
+
+ $effect(() => {
+ // This is just to force it to track messageRuns.
+ const _ = messageRuns;
+ document.querySelector('.message-run:last-child .message:last-child')?.scrollIntoView();
+ });
+
+ function handleKeydown(event) {
+ if (event.key === 'Escape') {
+ setLastRead(); // TODO: pass in "last message DT"?
+ }
+ }
+
+ let lastReadCallback = null;
+
+ function onscroll() {
+ clearTimeout(lastReadCallback); // Fine if lastReadCallback is null still.
+ lastReadCallback = setTimeout(setLastRead, 2 * 1000);
+ }
+
+ async function sendMessage(message) {
+ outbox.sendToConversation(conversationId, message);
+ }
+
+ async function deleteMessage(id) {
+ outbox.deleteMessage(id);
+ }
+</script>
+
+<svelte:window onkeydown={handleKeydown} />
+
+<div class="active-conversation" {onscroll} bind:this={activeConversation}>
+ {#each messageRuns as { sender, ownMessage, messages }}
+ <MessageRun
+ {sender}
+ class={{
+ ['own-message']: ownMessage,
+ ['other-message']: !ownMessage,
+ }}
+ >
+ {#each messages as message}
+ <Message
+ {...message}
+ editable={ownMessage}
+ {deleteMessage}
+ class={{
+ unsent: !message.id,
+ deleted: deleted.includes(message.id),
+ }}
+ />
+ {/each}
+ </MessageRun>
+ {/each}
+</div>
+<div class="create-message">
+ <MessageInput {sendMessage} />
+</div>